using System;
using System.Collections.Generic;
using System.Linq;
using Magiq.Support;

namespace Magiq.Update {
  public class EnumerableUpdate<T> : BaseUpdate<T> where T : class {
    private Dictionary<IPropertySet, Delegate> compiledChains;
    private Dictionary<IPropertySet, Delegate> compiledSettersValues;

    public EnumerableUpdate(IEnumerable<T> enumerable) {
      Where = enumerable;
    }

    public EnumerableUpdate(IEnumerable<T> enumerable, IEnumerable<IPropertySet> sets) : this(enumerable) {
      Where = enumerable;
      AddPorpertySets(sets);
    }

    public IEnumerable<T> Where { get; private set; }

    public override int Update() {
      if (PropertySets.Count == 0)
        return Where.Count();

      if (compiledSettersValues == null) {
        compiledSettersValues = new Dictionary<IPropertySet, Delegate>();
        compiledChains = new Dictionary<IPropertySet, Delegate>();

        foreach (var set in PropertySets) {
          compiledSettersValues.Add(set, set.ValueExpression.Compile());

          var expression = set.PropertyExpression.PreviousExpressionChain();

          if (expression != null)
            compiledChains.Add(set, expression.Compile());
        }
      }

      var updates = new List<object>();

      foreach (var item in Where) {
        foreach (var set in PropertySets) {
          var returnType = set.ValueExpression.Body.Type;

          object value = null;
          var valueGetted = false;

          if (set.PropertyType == returnType) {
            try {
              value = compiledSettersValues[set].DynamicInvoke(item);
              valueGetted = true;
            }
            catch (NullReferenceException) {}
          }
          else {
            returnType = set.PropertyType.IsGenericType &&
                         set.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)
                           ? set.PropertyType.GetGenericArguments().First()
                           : returnType;
            try {
              value = set.PropertyType == returnType
                        ? compiledSettersValues[set].DynamicInvoke(item)
                        :
                          Convert.ChangeType(compiledSettersValues[set].DynamicInvoke(item),
                                             set.PropertyType);
              valueGetted = true;
            }
            catch (NullReferenceException) {}
          }
          if (!valueGetted) continue;

          Delegate chain;
        var changed = false;
          
          var itemToUpdate = compiledChains.TryGetValue(set, out chain)
                               ? chain.DynamicInvoke(item)
                               : item;

          try {
            itemToUpdate.SetProperty(set.PropertyName, value);
            changed = true;
          }
          catch (NullReferenceException) { }

          if (changed && !updates.Contains(itemToUpdate))
            updates.Add(itemToUpdate);
        }
      }
      return updates.Count;
    }
  }
}